package sim.execution;

import java.util.Collections;
import java.util.LinkedList;
import java.util.Random;
import sim.coalitional.*;
import sim.helpers.Helper;
import sim.helpers.statistics.StatisticsHolder;
import sim.helpers.statistics.ValueHolder;
import sim.helpers.Variables;
import sim.helpers.comparers.CoordinatesComparer;
import sim.misc.DateUtils;

public class Executor implements Runnable {

    Thread runner;
    Random generator;
    private int offset;
    LinkedList<Node> nodes;
    LinkedList<Head> heads;
    LinkedList<Coalition> coalitions;
    LinkedList<Coordinates> area;
    Helper helper;
    StatisticsHolder repetitionNiSH;
    StatisticsHolder repetitionCcSH;
    StatisticsHolder repetitionAccSH;
    StatisticsHolder simNiSH;
    StatisticsHolder simCcSH;
    StatisticsHolder simAccSH;

    public Executor(int offset, Helper helper) {
        this.offset = offset;
        this.helper = helper;
        this.generator = new Random(System.nanoTime());
        simNiSH = new StatisticsHolder();
        simCcSH = new StatisticsHolder();
        simAccSH = new StatisticsHolder();
        runner = new Thread(this, "[E" + offset + "]");
        println("Started");
        runner.start();
    }

    public void run() {
        for (int var = Variables.minValue + offset * Variables.increment; var <= Variables.maxValue; var += Variables.increment * Variables.numberOfCores) {
            changeVariable(var);
            repetitionNiSH = new StatisticsHolder();
            repetitionCcSH = new StatisticsHolder();
            repetitionAccSH = new StatisticsHolder();
            for (int repetition = 0; repetition < Variables.repetitions; repetition++) {
                simulate();
                repetitionNiSH.addValue(new ValueHolder(repetition, getCurrentNetworkValue()));
                repetitionCcSH.addValue(new ValueHolder(repetition, getNumberOfCoalitions()));
                repetitionAccSH.addValue(new ValueHolder(repetition, getAcuracy()));
                cleanUp();
            }
            simNiSH.addValue(new ValueHolder(var, repetitionNiSH.getAvgValue()));
            simCcSH.addValue(new ValueHolder(var, repetitionCcSH.getAvgValue()));
            simAccSH.addValue(new ValueHolder(var, repetitionAccSH.getAvgValue()));
            repetitionNiSH = null;
            repetitionCcSH = null;
            repetitionAccSH = null;
        }
        writeStatistics();
        println("Done, will now exit");
    }

    private void changeVariable(int var) {
        if (Variables.changingConstant.trim().equalsIgnoreCase("qos")) {
            helper.QOS = var;
            println("Changing QOS -> " + var + "\t@ " + DateUtils.now(DateUtils.DATE_FORMAT_NOW_FULL));
        } else if (Variables.changingConstant.trim().equalsIgnoreCase("n")) {
            helper.N = var;
            println("Changing N -> " + var + "\t@ " + DateUtils.now(DateUtils.DATE_FORMAT_NOW_FULL));
        } else if (Variables.changingConstant.trim().equalsIgnoreCase("h")) {
            helper.H = var;
            println("Changing H -> " + var + "\t@ " + DateUtils.now(DateUtils.DATE_FORMAT_NOW_FULL));
        } else if (Variables.changingConstant.trim().equalsIgnoreCase("l")) {
            helper.L = var;
            println("Changing L -> " + var + "\t@ " + DateUtils.now(DateUtils.DATE_FORMAT_NOW_FULL));
        } else if (Variables.changingConstant.trim().equalsIgnoreCase("sd")) {
            helper.SD = var;
            println("Changing SD -> " + var + "\t@ " + DateUtils.now(DateUtils.DATE_FORMAT_NOW_FULL));
        } else {
            System.out.println("Did not find a valid changing constant to iterate, switching to QOS");
            helper.QOS = var;
            println("Changing QOS -> " + var + "\t@ " + DateUtils.now(DateUtils.DATE_FORMAT_NOW_FULL));
        }
    }

    private void simulate() {
        initializeLists();
        createNetwork();
//        printStats("******** INITIAL NETWORK ********");
        initializeClustering();
//        printStats("******** FIRST CLUSTERING ********");
        optimizeClustering();
//        printStats("******** OPTIMIZED CLUSTERING ********");
    }

    private void createNetwork() {
        setupHeads();
        setupNodes();
    }

    private void setupHeads() {
        for (int h = 0; h < helper.H; h++) {
            Coordinates c = randomCoordinates();
            if (alreadyPlaced(c)) {
                h--;
                continue;
            }
            heads.add(new Head(h, c));
            area.add(c);
        }
    }

    private void setupNodes() {
        for (int n = 0; n < helper.N; n++) {
            Coordinates c = randomCoordinates();
            if (alreadyPlaced(c)) {
                n--;
                continue;
            }
            nodes.add(new Node(n, c, helper.R, helper.Energy, this.helper));
            area.add(c);
            setClosestHead(nodes.getLast());
            coalitions.add(new Coalition(nodes.getLast().getId(), this.helper));
            coalitions.getLast().addNode(nodes.getLast());
            coalitions.getLast().setHead(nodes.getLast().getClosestHead());
        }
        for (Node node : nodes) {
            setupNeighbors(node);
        }
    }

    private void initializeClustering() {
        boolean flagChangesHappened = true;
        while (flagChangesHappened) {
            flagChangesHappened = false;
            for (Node node : nodes) {
                for (Node neighbor : node.getNeighbors()) {
                    if (getCoalition(node.getCoalitionId()) == null || getCoalition(neighbor.getCoalitionId()) == null) {
                        continue;
                    }
                    if (getCoalition(node.getCoalitionId()).getNodeList().size() == 0 || getCoalition(neighbor.getCoalitionId()).getNodeList().size() == 0) {
                        continue;
                    }
                    if (node.getCoalitionId() == neighbor.getCoalitionId()) {
                        continue;
                    }
                    if (!(getCoalition(node.getCoalitionId()).getNodeList().size() == 1) && !(getCoalition(neighbor.getCoalitionId()).getNodeList().size() == 1)) {
                        continue; // One of the two should be a singleton coalition, according to the merge and split rules
                    }
                    if (checkNodeMergeWith(node, neighbor)) {
                        flagChangesHappened = true;
                        int neighborCoalitionId = neighbor.getCoalitionId();
                        getCoalition(node.getCoalitionId()).mergeWith(getCoalition(neighborCoalitionId));
                        coalitions.removeFirstOccurrence(getCoalition(neighborCoalitionId));
                        break;
                    }
                }
                if (flagChangesHappened) {
                    break;
                }
            }
        }
    }

    private void optimizeClustering() {
        boolean flagChangesHappened = true;
        while (flagChangesHappened) {
            flagChangesHappened = false;
            for (Coalition coalition : coalitions) {
                for (Coalition candidate : coalitions) {
                    if (coalition == null || candidate == null || coalition.getNodeList() == null || candidate.getNodeList() == null) {
                        continue;
                    }
                    if (coalition.getId() == candidate.getId()) {
                        continue;
                    }
                    if (coalition.getNodeList().size() < 1) {
                        coalition = null;
                        if (!coalitions.remove(coalition)) {
                            println("Could not remove empty coalition..");
                        }
                        continue;
                    }
                    if (candidate.getNodeList().size() < 1) {
                        candidate = null;
                        if (!coalitions.remove(candidate)) {
                            println("Could not remove empty candidate coalition");
                        }
                        continue;
                    }
                    if (coalition.checkMergingWith(candidate)) {
                        coalition.mergeWith(candidate);
                        flagChangesHappened = true;
                        coalitions.removeFirstOccurrence(candidate);
                        candidate = null;
                        break;
                    }
                }
                if (flagChangesHappened) {
                    break;
                }
            }
        }
    }

    private void setupNeighbors(Node n) {
        for (Node node : nodes) {
            if (n.isWithinRadius(node)) {
                n.addNeighbor(node);
                node.addNeighbor(n);
            }
        }
    }

    private boolean checkNodeMergeWith(Node a, Node b) {
        return getCoalition(a.getCoalitionId()).checkMergingWith(getCoalition(b.getCoalitionId()));
    }

    private void cleanUp() {
        nodes = null;
        heads = null;
        coalitions = null;
        area = null;
    }

    private void writeStatistics() {
        Variables.NLiSH.getValueList().addAll(simNiSH.getValueList());
        Variables.CcSH.getValueList().addAll(simCcSH.getValueList());
        Variables.AccSH.getValueList().addAll(simAccSH.getValueList());
    }

    private double getCurrentNetworkValue() {
        double ret = 0;
        return nodes.size() / getNumberOfCoalitions();
    }

    private double getNumberOfCoalitions() {
        int ret = 0;
        for (Coalition coalition : coalitions) {
            if (coalition == null) {
                continue;
            } else if (coalition.getNodeList() == null) {
                continue;
            } else {
                ret++;
            }
        }
        return ret;
    }

    private double getAcuracy(){
        double ret = 0;
        for (Coalition coalition : coalitions) {
            ret += helper.pdf(coalition.getAvgDistanceMetric(), helper.SD);
        }
        return ret / getNumberOfCoalitions();
    }
    
    private void initializeLists() {
        nodes = new LinkedList<Node>();
        heads = new LinkedList<Head>();
        coalitions = new LinkedList<Coalition>();
        area = new LinkedList<Coordinates>();
    }

    private int getNodeViaCoordinates(Coordinates c) {
        int ret = -1;
        for (Node node : nodes) {
            if (node.getCoor().getX() == c.getX() && node.getCoor().getY() == c.getY()) {
                ret = node.getId();
                break;
            }
        }
        return ret;
    }

    private int getHeadViaCoordinates(Coordinates c) {
        int ret = -1;
        for (Head head : heads) {
            if (head.getCoor().getX() == c.getX() && head.getCoor().getY() == c.getY()) {
                ret = head.getId();
                break;
            }
        }
        return ret;
    }

    private int getCoalitionViaCoordinates(Coordinates c) {
        int ret = -1;
        for (Node node : nodes) {
            if (node.getCoor().getX() == c.getX() && node.getCoor().getY() == c.getY()) {
                ret = node.getCoalitionId();
                break;
            }
        }
        return ret;
    }

    private Coordinates randomCoordinates() {
        return new Coordinates(generator.nextInt(helper.L), generator.nextInt(helper.L));
    }

    private void setClosestHead(Node n) {
        double minDistance = 2 * helper.L;
        for (Head h : heads) {
            double currentDistance = helper.distance(n, h);
            if (currentDistance < minDistance) {
                minDistance = currentDistance;
                n.setClosestHead(h);
            }
        }
        return;
    }

    private Coalition getCoalition(int id) {
        Coalition ret = null;
        for (Coalition coalition : coalitions) {
            if (coalition.getId() == id) {
                ret = coalition;
                break;
            }
        }
        return ret;
    }

    private boolean alreadyPlaced(Coordinates candidate) {
        boolean ret = false;
        for (Coordinates coordinates : area) {
            if ((coordinates.getX() == candidate.getX()) && (coordinates.getY() == candidate.getY())) {
                ret = true;
                break;
            }
        }
        return ret;
    }

    private void printCoalitions() {
        System.out.print(runner.getName() + ": The following " + coalitions.size() + " coalitions exist:");
        for (Coalition c : coalitions) {
            System.out.print(" " + c.getId());
        }
        System.out.println("");
        for (Coalition c : coalitions) {
            if (!(c.getNodeList() == null)) {
                c.printMembers();
            } else {
                println("Coalition " + c.getId() + " has no nodes");
            }
        }
        System.out.println("\n");
    }

    private void printNetwork() {
        Collections.sort(area, new CoordinatesComparer());
        if (helper.L > 20) {
            println("This function is given for testing purposes and does not support large areas.\nArea too large (>20), not printing coalitions.");
            return;
        }
        String[][] tmpAreaR = new String[helper.L][helper.L];
        for (int i = 0; i < tmpAreaR.length; i++) {
            for (int j = 0; j < tmpAreaR.length; j++) {
                tmpAreaR[i][j] = "   ";
            }
        }
        for (Coordinates c : area) {
            String tmpEntity = (getNodeViaCoordinates(c) != -1 ? String.valueOf(getNodeViaCoordinates(c)) : "H" + String.valueOf(getHeadViaCoordinates(c)));
            tmpAreaR[c.getX()][c.getY()] = tmpEntity;
        }
        for (int i = 0; i < tmpAreaR.length; i++) {
            for (int j = 0; j < tmpAreaR.length; j++) {
                String tmpS = tmpAreaR[i][j];
                if (tmpS.length() == 1) {
                    tmpS += "  ";
                } else if (tmpS.length() == 2) {
                    tmpS += " ";
                }
                System.out.print(tmpS + " ");
            }
            System.out.println("");
        }
        System.out.println("");
    }

    private void printNetworkCoalitions() {
        Collections.sort(area, new CoordinatesComparer());
        if (helper.L > 20) {
            println("This function is given for testing purposes and does not support large areas.\nArea too large (>20), not printing coalitions.");
            return;
        }
        String[][] tmpAreaR = new String[helper.L][helper.L];
        for (int i = 0; i < tmpAreaR.length; i++) {
            for (int j = 0; j < tmpAreaR.length; j++) {
                tmpAreaR[i][j] = "   ";
            }
        }
        for (Coordinates c : area) {
            String tmpEntity = (getCoalitionViaCoordinates(c) != -1 ? String.valueOf(getCoalitionViaCoordinates(c)) : "H" + String.valueOf(getHeadViaCoordinates(c)));
            tmpAreaR[c.getX()][c.getY()] = tmpEntity;
        }
        for (int i = 0; i < tmpAreaR.length; i++) {
            for (int j = 0; j < tmpAreaR.length; j++) {
                String tmpS = tmpAreaR[i][j];
                if (tmpS.length() == 1) {
                    tmpS += "  ";
                } else if (tmpS.length() == 2) {
                    tmpS += " ";
                }
                System.out.print(tmpS + " ");
            }
            System.out.println("");
        }
        System.out.println("");
    }

    private void printStats(String str) {
        println("Maximum intracoalition distance D is " + helper.getMaximumIntraCoalitionDistance() + ".");
        println(str);
        printNetworkCoalitions();
        printCoalitions();
    }

    private void println(String str) {
        System.out.println(runner.getName() + ": " + str);
    }

    private void print(String str) {
        System.out.print(runner.getName() + ": " + str);
    }

// <editor-fold desc="Test" defaultstate="collapsed">
//    private void initializeClustering_test() {
//        updateMergeLists();
//        performMerges();
//    }
//
//    private void optimizeClustering_test() {
//        boolean flagChangesHappened = true;
//        while (flagChangesHappened) {
//            flagChangesHappened = false;
//            for (Coalition coalition : coalitions) {
//                for (Coalition candidate : coalitions) {
//                    if (coalition == null || candidate == null || coalition.getNodeList() == null || candidate.getNodeList() == null) {
//                        continue;
//                    }
//                    if (coalition.getId() == candidate.getId()) {
//                        continue;
//                    }
//                    if (coalition.getNodeList().size() < 1) {
//                        coalition = null;
//                        if (!coalitions.remove(coalition)) {
//                            println("Could not remove empty coalition..");
//                        }
//                        continue;
//                    }
//                    if (candidate.getNodeList().size() < 1) {
//                        candidate = null;
//                        if (!coalitions.remove(candidate)) {
//                            println("Could not remove empty candidate coalition");
//                        }
//                        continue;
//                    }
//                    if (coalition.checkMergingWith(candidate)) {
//                        coalition.mergeWith(candidate);
//                        flagChangesHappened = true;
//                        coalitions.removeFirstOccurrence(candidate);
//                        candidate = null;
//                        break;
//                    }
//                }
//                if (flagChangesHappened) {
//                    break;
//                }
//            }
//        }
//    }
//
//    private void performMerges() {
//        boolean flag = true;
//        while (flag) {
//            flag = false;
//            for (Coalition ca : this.coalitions) {
//                if (ca == null) {
//                    flag = true;
//                    coalitions.remove(ca);
//                    break;
//                }
//                if (ca.getCoalitionMergeList().size() == 0) {
//                    continue;
//                }
////                System.out.println("");
//                checkMergeFor(ca);
//                flag = true;
//                break;
//            }
//        }
//    }
//
//    private void updateMergeLists() {
//        for (Coalition ca : coalitions) {
//            for (Coalition cb : coalitions) {
//                if (ca == null || cb == null) {
//                    continue;
//                }
//                if (ca.getId() == cb.getId()) {
//                    continue;
//                }
//                if (ca.getNodeList().size() == 0 || cb.getNodeList().size() == 0) {
//                    continue;
//                }
//                if (ca.checkMergingWith(cb)) {
//                    ca.addToMergeList(cb);
//                    cb.addToMergeList(ca);
//                }
//            }
////            System.out.println("Coalition " + ca.getId() + " to be merged with: " + ca.getMergeListString());
//        }
//    }
//
//    private boolean checkMergeFor(Coalition ca) {
//        boolean flag = true;
//        while (flag) {
//            flag = false;
////            System.out.println("");
//            updateMergeLists();
////            System.out.println("");
//            for (Coalition coalition : ca.getCoalitionMergeList()) {
//                if (ca.getId() == coalition.getCoalitionMergeList().getFirst().getId()) {
//                    ca.mergeWith(coalition);
////                    println("Merged " + ca.getId() + " with " + coalition.getId());
////                    ca.printMembers();
//                    removeMerged(coalition);
//                    coalitions.remove(coalition);
//                    coalition = null;
//                    flag = true;
//                    break;
//                }
////                println("\nRedirecting from " + ca.getId() + " to " + ca.getCoalitionMergeList().getFirst().getId() + "\n");
//                return checkMergeFor(ca.getCoalitionMergeList().getFirst());
//            }
//        }
//        return flag;
//    }
//
//    private void printDistances() {
//        if (nodes.size() > 20) {
//            println("Node number is beyond testing purposes (>20). Distance printing is going to be omitted.");
//            return;
//        }
//        System.out.print("DISTANCES:\n    ");
//        for (int i = 0; i < nodes.size(); i++) {
//            System.out.print(i + "     ");
//        }
//        System.out.println("");
//        for (Node node : nodes) {
//            System.out.print(node.getId() + "  ");
//            for (Node node1 : nodes) {
//                System.out.print(Double.toString(helper.distance(node, node1)).substring(0, 3) + "   ");
//            }
//            System.out.println("");
//        }
//        System.out.println("");
//    }
//
//    private void removeMerged(Coalition coalition) {
//        for (Coalition ca : coalitions) {
//            for (Coalition cb : ca.getCoalitionMergeList()) {
//                if (coalition == cb) {
//                    ca.getCoalitionMergeList().remove(cb);
//                    break;
//                }
//            }
//        }
//    }
}

//</editor-fold>
